package cnt5106.p2p.peer.network;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Vector;

import cnt5106.p2p.common.protocol.Message;
import cnt5106.p2p.common.protocol.MessageType;;

public class PeerServer extends Peer implements Runnable {

	private Peer peer;
	private DatagramSocket serverUDPSocket;
	
	public DatagramSocket getServerUDPSocket() {
		return serverUDPSocket;
	}

	private static HashMap<InetSocketAddress, Boolean> serveList = new HashMap<InetSocketAddress, Boolean>();
	private ServerSocket peerServerTCPSocket = null;
	private Socket ownerTCPSocket = null;
	
	public Vector<InetSocketAddress> getPeerList() {
		return peer.peerList;
	}

	public void setPeerList(Vector<InetSocketAddress> peerList) {
		this.peer.peerList = peerList;
	}
	
	private void formServeList(){
		Vector<InetSocketAddress> peerlist = getPeerList();
		for (InetSocketAddress peerAddr : peerlist){
			serveList.put(peerAddr, false);
		}
	}
	
	private void setServeList(InetSocketAddress addr){
		serveList.put(addr, true);
	}
	
	public PeerServer(Peer peer) {
		this.peer = peer;
		try {
			serverUDPSocket = new DatagramSocket(peer.serverUDPPort);
		} catch (SocketException e) {
			e.printStackTrace();
		}
	}

	public void run() {
		
		boolean finished = false;
		byte[] buf = new byte[150];
		DatagramPacket reply = null;
		Message notifyNewUserReply = null;
		HashSet<SocketAddress> peerSet = new HashSet<SocketAddress> ();
		
		while (true) {
			try {
				/*if (requestQueue.isEmpty()){
					reply = new DatagramPacket(buf, buf.length);
					serverUDPSocket.receive(reply);
					requestQueue.add((DatagramPacket) reply);
				}
				reply = requestQueue.firstElement();
				notifyNewUserReply = Message.parse(reply.getData());
				requestQueue.remove(0);*/
				
				reply = new DatagramPacket(buf, buf.length);
				serverUDPSocket.receive(reply);
				notifyNewUserReply = Message.parse(reply.getData());
				
				switch (notifyNewUserReply.getType()) {
				
				case TRACKER_NOTIFY_NEW_PEER:
					System.out.println("New peer " + notifyNewUserReply.getData() + " joined the peer group");
					peer.peerList.add((InetSocketAddress) notifyNewUserReply.getData());
					
					if (peer.STATE == PEER_STATE.FILEOWNER) {
						System.out.println("Current Peer List are " + peer.peerList);
						System.out.print("peer> ");
					}
					break;
					
				case PEER_TCPPORT_REQUEST:
					formServeList();  // this is a patch for determining when to finish
					peerServerTCPSocket = new ServerSocket(peer.serverTCPPort);
					InetSocketAddress temp = new InetSocketAddress("127.0.0.1",peer.serverTCPPort);
					Message msg = Message.generate(MessageType.PEER_TCPPORT_REPLY, "", 1, temp);
					serverUDPSocket.send(new DatagramPacket(msg.toByte(), msg.toByte().length, reply.getAddress(), reply.getPort()));
					ownertoPeerDataTransfer();
					if (peer.peerList.size() == 0)
						finished = true;
					break;
					
				case PEER_REQUEST_CHUNK:
				case PEER_DATA_ACK:
					peertoPeerDataTransfer(notifyNewUserReply.getNumber(), 
							notifyNewUserReply.getSeqNo(), reply.getAddress(), reply.getPort());
					peerSet.add(reply.getSocketAddress());
					break;
					
				case PEER_FINISH:
					setServeList((InetSocketAddress)notifyNewUserReply.getData());
					finished = (!serveList.containsValue(false));
					break;
					
				default:
					break;
				}
				
			} catch (SocketException e) {
				if (serverUDPSocket.isClosed()){
					System.out.println("Peer Sever finished.");
					return;
				}
				else e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			if (finished)         // quit the while(true) loop
				break;
		}
		serverUDPSocket.close();
		
	}

	public void receiveNewPeerNotify() {
	}

	public void ownertoPeerDataTransfer() throws IOException {
		// Start TCP server, wait for new chunk
		synchronized(peer.lock){
			new OwnerChunkReceiver(peer).run();
			peer.lock.notifyAll();
		}
	}

	/**
	 * peer sever UDP to listening state, response for incoming request
	 * @throws IOException 
	 */
	public void peertoPeerDataTransfer(int chunkNum, int subChunkNum, InetAddress peerIP, int peerPort) throws IOException {
		byte[] chunk = null;
		byte[] subChunk = null;
		
		int subChunkSize = 0;
		int from = 0, to = 0;
		byte[] data = null;
		Message msg = null;
		DatagramPacket toPeer = null;
		
		if ((chunk = peer.requestedFileControler.readFile(chunkNum, false)) != null){
			subChunkSize = (chunk.length / peer.subChunkNum) + 1; 

			from = subChunkNum * subChunkSize;
			to = (from + subChunkSize > chunk.length) ? chunk.length : (from + subChunkSize);
			
			subChunk = Arrays.copyOfRange(chunk, from, to);
			msg = Message.generate(MessageType.PEER_POSITIVE_REPLY, 
					peer.requestedFile, chunkNum, subChunkNum, subChunk);
			data = msg.toByte();
			toPeer = new DatagramPacket(data, data.length, peerIP, peerPort);
			serverUDPSocket.send(toPeer);
			// Utils.logMessage("UDP Server", serverUDPSocket.getLocalSocketAddress(), true, toPeer.getSocketAddress(), msg);
			// logger.addLog("UDP Server", serverUDPSocket.getLocalSocketAddress(), true, toPeer.getSocketAddress(), msg);
			
			/*for(int i=0; i<peer.subChunkNum; i++){
				subChunk = Arrays.copyOfRange(chunk, from, to);
				msg = Message.generate(MessageType.PEER_POSITIVE_REPLY, peer.requestedFile, chunkNum, i, subChunk);
				data = msg.toByte();
				toPeer = new DatagramPacket(data, data.length, peerIP, peerPort);
				serverUDPSocket.send(toPeer);
				Utils.logMessage("UDP Server", serverUDPSocket.getLocalSocketAddress(), true, toPeer.getSocketAddress(), msg);
				
				fromPeer = new DatagramPacket(buff, buff.length);
				serverUDPSocket.receive(fromPeer);
				msg = Message.parse(fromPeer.getData());
				Utils.logMessage("UDP Server", serverUDPSocket.getLocalSocketAddress(), false, fromPeer.getSocketAddress(), msg);
					
				while (msg.getType() != MessageType.PEER_DATA_ACK) {
					//System.out.println("ACK Error" + msg.getType().toString()); ////////////////////////
					//System.exit(1);
					requestQueue.add(fromPeer);
					fromPeer = new DatagramPacket(buff, buff.length);
					serverUDPSocket.receive(fromPeer);
					msg = Message.parse(fromPeer.getData());
					Utils.logMessage("UDP Server", serverUDPSocket.getLocalSocketAddress(), false, fromPeer.getSocketAddress(), msg);
				}
				
				from += subChunkSize;
				to = ((to + subChunkSize) > chunk.length) ? chunk.length : (to + subChunkSize);
			}*/
			//System.out.println("Send chunk #" + chunkNum + " to peer "+ peerIP + "," + peerPort);
			
			
		} else {
			msg = Message.generate(MessageType.PEER_NEGATIVE_REPLY, peer.requestedFile, chunkNum, "");
			data = msg.toByte();
			toPeer = new DatagramPacket(data, data.length, peerIP, peerPort);
			serverUDPSocket.send(toPeer);
			// TODO Utils.logMessage("UDP Server", serverUDPSocket.getLocalSocketAddress(), true, toPeer.getSocketAddress(), msg);
			// logger.addLog("UDP Server", serverUDPSocket.getLocalSocketAddress(), true, toPeer.getSocketAddress(), msg);
		}
	}

	/**
	 * receive owner chunk from owner
	 */
	class OwnerChunkReceiver implements Runnable {
		private Peer peer;
		private BufferedInputStream fromFileOwner;
		private BufferedOutputStream toFileOwner;
		private Message msg = null;

		public OwnerChunkReceiver(Peer peer) throws IOException {
			this.peer = peer;
			ownerTCPSocket = peerServerTCPSocket.accept();
			fromFileOwner = new BufferedInputStream(ownerTCPSocket.getInputStream());
			toFileOwner = new BufferedOutputStream (new DataOutputStream(ownerTCPSocket.getOutputStream()));

		}

		public void run() {
			try {
				// accepting data
				byte[] buff = new byte[1024*101];
				int count = 0;
				int endPoint = 0;
				boolean loop = true;
				byte[] ack = null;
				
				while (loop){
					endPoint = 0;
					while(!Message.verifyMessageCompletion(Arrays.copyOfRange(buff, 0, endPoint))) {
						while (fromFileOwner.available() != 0){
							count = fromFileOwner.read(buff, endPoint, (buff.length - endPoint));
							endPoint += count;
							}
						if (endPoint == 0)
							continue;
						msg = Message.parse(Arrays.copyOfRange(buff, 0, endPoint));
					}
					
					switch (msg.getType()){
					case PEER_FILEOWNER_DATAPACKET:
						//  write chunk to file
						peer.requestedFileControler.writeFile((byte[]) msg.getData(), msg.getNumber());
						System.out.println("Receive chunk #" + msg.getNumber() + " from the file owner.");
						//  send ACK to file owner
						msg = Message.generate(MessageType.PEER_DATA_ACK, "", msg.getNumber(), null);
						ack = msg.toByte();
						toFileOwner.write(ack);
						toFileOwner.flush();
						
						break;
					case PEER_FILEOWNER_END:
						fromFileOwner.close();
						ownerTCPSocket.close();
						peerServerTCPSocket.close();
						// print the index of chunk obtained from file owner, just for test
						//peer.requestedFileControler.testPrintChunkList();  
						
						loop = false;
						break;		
					default:
						System.out.println("Errors occured in receiving file owner chunks");
						System.exit(1);
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				try {
					peerServerTCPSocket.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
